<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>不定高度的虚拟列表</title>
  </head>
  <body>
    <style>
      .list {
        height: 400px;
        width: 300px;
        outline: 1px solid seagreen;
        overflow-x: hidden;
      }
      .list-item {
        outline: 1px solid red;
        outline-offset: -2px;
        background-color: #fff;
      }
    </style>
    <div class="list">
      <div class="list-inner"></div>
    </div>
    <script>
      // 快速移动滚动条，中间未渲染部分，导致渲染后高度偏移差问题
      // 参考链接：https://lkangd.com/post/virtual-infinite-scroll/
      // const throttle = (callback) => {
      //     let isThrottled = false;
      //     return (...args)=> {
      //         if (isThrottled) return;
      //             callback.apply(this, args);
      //         isThrottled = true;
      //         requestAnimationFrame(() => {
      //             isThrottled = false;
      //         });
      //     }
      // }

      // function run(task, taskEndCallback) {
      //     let oldDate = Date.now();
      //     requestAnimationFrame(() => {
      //         let now = Date.now();
      //         console.log(now - oldDate)
      //         if(now - oldDate <= 16.5) {
      //             const result = task();
      //             taskEndCallback(result);
      //         }else {
      //             run(task, render);
      //         }
      //     })
      // }

      // function debounce(callback) {
      //     let timerId;
      //     return function() {
      //         if (timerId) {
      //             cancelAnimationFrame(timerId);
      //         }
      //         timerId = requestAnimationFrame(() => {
      //             callback.apply(this, arguments);
      //         });
      //     };
      // }

      function throttle(callback) {
        let requestId;
        return (...args) => {
          if (requestId) {
            return;
          }
          requestId = requestAnimationFrame(() => {
            callback.apply(this, args);
            requestId = null;
          });
        };
      }

      const randomIncludes = (min, max) => {
        return Math.floor(Math.random() * (max - min + 1) + min);
      };

      const clientHeight = 400;
      const listEl = document.querySelector(".list");
      const listInner = document.querySelector(".list-inner");

      function initAutoSizeVirtualList(props) {
        const cache = [];
        window.cache = cache;
        let oldFirstIndex = 0;
        const { listEl, listInner, minSize = 30, clientHeight, items } = props;
        // 默认情况下可见数量
        const viewCount = Math.ceil(clientHeight / minSize);
        // 缓存区数量
        const bufferSize = 6;
        listEl.style.cssText += `height:${clientHeight}px;overflow-x: hidden`;

        // const findItemIndex = (startIndex, scrollTop) => {
        //     scrollTop === undefined && (
        //         scrollTop = startIndex,
        //         startIndex = 0
        //     )
        //     let totalSize = 0;
        //     for(let i = startIndex; i < cache.length; i++) {
        //         totalSize += cache[i].height;
        //         if(totalSize >= scrollTop || i == cache.length - 1) {
        //             return i;
        //         }
        //     }
        //     return startIndex;
        // }

        // 二分查询优化
        const findItemIndex = (scrollTop) => {
          let low = 0;
          let high = cache.length - 1;
          while (low <= high) {
            const mid = Math.floor((low + high) / 2);
            const { top, bottom } = cache[mid];
            if (scrollTop >= top && scrollTop <= bottom) {
              high = mid;
              break;
            } else if (scrollTop > bottom) {
              low = mid + 1;
            } else if (scrollTop < top) {
              high = mid - 1;
            }
          }
          return high;
        };

        // 更新每个item的位置信息
        const upCellMeasure = () => {
          const listItems = listInner.querySelectorAll(".list-item");
          if (listItems.length === 0) {
            return;
          }

          const firstItem = listItems[0];
          const firstIndex = +firstItem.dataset.index;
          const lastIndex = +listItems[listItems.length - 1].dataset.index;
          // 解决向上缓慢滚动时，高度存在的偏移差问题，非常重要
          if (firstIndex < oldFirstIndex && !cache[firstIndex].isUpdate) {
            const dHeight =
              firstItem.getBoundingClientRect().height -
              cache[firstIndex].height;
            listEl.scrollTop += dHeight;
          }
          [...listItems].forEach((listItem) => {
            const rectBox = listItem.getBoundingClientRect();
            const index = listItem.dataset.index;
            const prevItem = cache[index - 1];
            const top = prevItem ? prevItem.bottom : 0;
            Object.assign(cache[index], {
              height: rectBox.height,
              top,
              bottom: top + rectBox.height,
              isUpdate: true,
            });
          });
          // 切记一定要更新未渲染的listItem的top值
          for (let i = lastIndex + 1; i < cache.length; i++) {
            const prevItem = cache[i - 1];
            const top = prevItem ? prevItem.bottom : 0;
            Object.assign(cache[i], {
              top,
              bottom: top + cache[i].height,
            });
          }
          oldFirstIndex = firstIndex;
        };

        const getTotalSize = () => {
          return cache[cache.length - 1].bottom;
        };
        const getStartOffset = (startIndex) => {
          return cache[startIndex].top;
        };
        const getEndOffset = (endIndex) => {
          return cache[endIndex].bottom;
        };

        // 缓存位置信息
        items.forEach((item, i) => {
          cache.push({
            index: i,
            height: minSize,
            top: minSize * i,
            bottom: minSize * i + minSize,
            isUpdate: false,
          });
        });

        return function autoSizeVirtualList(renderItem, rendered) {
          const startIndex = findItemIndex(listEl.scrollTop);
          const endIndex = startIndex + viewCount;
          // const visiblityEndIndex = findItemIndex(clientHeight + listEl.scrollTop);
          const startBufferIndex = Math.max(0, startIndex - bufferSize);
          const endBufferIndex = Math.min(
            items.length - 1,
            endIndex + bufferSize
          );
          const renderItems = [];
          for (let i = startBufferIndex; i <= endBufferIndex; i++) {
            renderItems.push(renderItem(items[i], cache[i]));
          }
          const startOffset = getStartOffset(startBufferIndex);
          const endOffset = getTotalSize() - getEndOffset(endBufferIndex);
          rendered(renderItems);
          // 渲染完成后，才更新缓存的高度信息
          upCellMeasure();
          listInner.style.setProperty(
            "padding",
            `${startOffset}px 0 ${endOffset}px 0`
          );
        };
      }

      // 模拟1万条数据
      const count = 10000;
      const items = Array.from({ length: count }).map((item, i) => ({
        name: `item ${i}`,
        height: randomIncludes(40, 120),
      }));
      const autoSizeVirtualList = initAutoSizeVirtualList({
        listEl,
        listInner,
        clientHeight,
        items,
      });

      document.addEventListener("DOMContentLoaded", () => {
        autoSizeVirtualList(
          (item, rectBox) => {
            return `<div class="list-item" data-index="${rectBox.index}" style="height:${item.height}px">${item.name}</div>`;
          },
          (renderItems) => {
            listInner.innerHTML = renderItems.join("");
          }
        );
      });

      listEl.addEventListener(
        "scroll",
        throttle(() => {
          console.log('xxxxx');
          autoSizeVirtualList(
            (item, rectBox) => {
              return `<div class="list-item" data-index="${rectBox.index}" style="height:${item.height}px">${item.name}</div>`;
            },
            (renderItems) => {
              listInner.innerHTML = renderItems.join("");
            }
          );
        })
      );
    </script>
  </body>
</html>
